using System.Windows.Forms;
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using Barrage;
using Barrage.Render;
using SlimDX.Direct3D9;
using NLog;
using NLog.Targets;
using OpenALDotNet;
using SlimDX;
using NLog.Targets.Wrappers;
using Barrage.Textures;
using Barrage.Meshes.Text;
using System.Threading;

namespace Barrage.Global
{
    /// <summary>
    /// Event used to dispose static resources, like BaseShader or resource managers
    /// </summary>
    public delegate void EngineDisposeHandler();
    /// <summary>
    /// Event used to dispose static resources, like BaseShader or resource managers
    /// </summary>
    public delegate void EngineDisposeDefaultPoolResourcesHandler();
    /// <summary>
    /// Barrage Engine main class. Inits libraries and manages game loop
    /// </summary>
    public class Engine : IDisposable
    {
        event EngineDisposeHandler OnDispose;
        event EngineDisposeHandler OnDisposeDefaultPool;
        event EngineDisposeHandler OnRecreateDefaultPool;
        /// <summary>
        /// Adds a new handler to the dispose event to clear static resources
        /// </summary>
        /// <param name="handler">Handler that will be registered</param>
        public static void RegisterDisposeHandler(EngineDisposeHandler handler)
        {
            Instance.OnDispose += handler;
        }
        /// <summary>
        /// Removes a handler from the dispose event
        /// </summary>
        /// <param name="handler">Handler that will be unregistered</param>
        public static void UnregisterDisposeHandler(EngineDisposeHandler handler)
        {
            Instance.OnDispose -= handler;
        }
        /// <summary>
        /// Adds a new handler to the dispose event to clear static resources
        /// </summary>
        /// <param name="handler">Handler that will be registered</param>
        public static void RegisterDisposeDefaultPoolHandler(EngineDisposeHandler handler)
        {
            Instance.OnDisposeDefaultPool += handler;
        }
        /// <summary>
        /// Removes a handler from the dispose event
        /// </summary>
        /// <param name="handler">Handler that will be unregistered</param>
        public static void UnregisterDisposeDefaultPoolHandler(EngineDisposeHandler handler)
        {
            Instance.OnDisposeDefaultPool -= handler;
        }
        /// <summary>
        /// Adds a new handler to the dispose event to clear static resources
        /// </summary>
        /// <param name="handler">Handler that will be registered</param>
        public static void RegisterRecreateDefaultPoolHandler(EngineDisposeHandler handler)
        {
            Instance.OnRecreateDefaultPool += handler;
        }
        /// <summary>
        /// Removes a handler from the dispose event
        /// </summary>
        /// <param name="handler">Handler that will be unregistered</param>
        public static void UnregisterRecreateDefaultPoolHandler(EngineDisposeHandler handler)
        {
            Instance.OnRecreateDefaultPool -= handler;
        }
        /// <summary>
        /// File where the engine configuration is stored
        /// </summary>
        public const string CONFIG_FILE = "barrage.xml";
        
        Device device;
        /// <summary>
        /// Direct3D Device of the engine
        /// </summary>
        public static Device Device { get { return Instance.device; } }


        Game game;
        Form mainForm;

        /// <summary>
        /// Instance of the engine. It doesn't create the instance if there's no engine loaded (It would need a Form handle)
        /// </summary>
        public static Engine Instance { get { return instance; } }
        static Engine instance;

        EngineConfig engineConfig;
        /// <summary>
        /// Gets the Engine configuration, with data such as resolution, fullscreen, etc.
        /// </summary>
        public static EngineConfig Config { get { return Instance.engineConfig; } }

        /// <summary>
        /// Returns game form width
        /// </summary>
        public int WindowWidth { get { return mainForm.Width; } }
        /// <summary>
        /// Returns game form height
        /// </summary>
        public int WindowHeight { get { return mainForm.Height; } }

        List<DisplayMode> displayModes = new List<DisplayMode>(10);
        int currentDisplayMode;
        /// <summary>
        /// List of supported display modes for the engine
        /// </summary>
        public static List<DisplayMode> DisplayModes { get { return Instance.displayModes; } }
        /// <summary>
        /// Currently selected display mode index in the supported list
        /// </summary>
        public static int CurrentDisplayModeIndex { get { return Instance.currentDisplayMode; } }
        /// <summary>
        /// Currently selected display mode
        /// </summary>
        public static DisplayMode CurrentDisplayMode { get { return DisplayModes[CurrentDisplayModeIndex]; } }

        Surface defaultDepthBuffer;
        /// <summary>
        /// Depth buffer used in the backbuffer that can be used for other purposes
        /// </summary>
        public static Surface DefaultDepthBuffer { get { return Instance.defaultDepthBuffer; } }
        
        Surface backBuffer;
        /// <summary>
        /// Backbuffer surface that must be used to render to screen
        /// </summary>
        public static Surface BackBuffer { get { return Instance.backBuffer; } }

        SurfaceDescription defaultRenderTargetDescription;
        /// <summary>
        /// Descriptor of the backbuffer surface
        /// </summary>
        public static SurfaceDescription DefaultRenderTargetDescription { get { return Instance.defaultRenderTargetDescription; } }

        bool finished = false;
        /// <summary>
        /// Tells if the application is considered to be finished and it should start closing
        /// </summary>
        public bool Finished { get { return finished; } }

        PresentParameters presentParameters;

        /// <summary>
        /// Inits the engine
        /// </summary>
        /// <param name="form">A Form that will be used to render the game</param>
        public Engine(Form form)
        {
            if (instance!=null) throw new Exception("An instance of Barrage Engine already exists");
            this.mainForm = form;
            instance = this;
            initConfig();
            initLogger();
            initD3D();
            initAudio();
            initKeyboard();
            initMouse();
            initPaths();
            SlimDX.Configuration.DetectDoubleDispose = true;
            SlimDX.Configuration.EnableObjectTracking = true;
            NLog.LogManager.GetLogger("Barrage::D3DEngine").Debug("Init D3D Engine");           
        }

        void initConfig(){
            engineConfig = new EngineConfig(CONFIG_FILE);
        }

        void initAudio()
        {
            OpenAudioLibrary.AlutInit();
            // Orientation doesn't seem to have any effect...
            //AudioListener.Orientation = new OpenALDotNet.Orientation(new Vector3(0, 0, 1), new Vector3(0, 1, 0));
            AudioListener.Position = Vector3.Zero;
            AudioContext.SpeedOfSound = 2;
        }

        void initPaths()
        {
            Store.PathConfig.Add(Store.SHADER_PATH,"Shaders/");
            Store.PathConfig.Add(Store.TEXTURE_PATH,"Textures/");
            Store.PathConfig.Add(Store.MODEL_PATH,"Models/");
            Store.PathConfig.Add(Store.LANGUAGE_PATH, "Texts/");
            Store.PathConfig.Add(Store.LAYOUT_PATH, "Layouts/");
            Store.PathConfig.Add(Store.FONT_PATH, "Fonts/");
            Store.PathConfig.Add(Store.FONT_TEXTURE_PATH, "Fonts/");
            Store.PathConfig.Add(Store.TEMPLATE_PATH, "Templates/");
            Store.PathConfig.Add(Store.AUDIO_PATH, "Audio/");
            Store.PathConfig.Add(Store.MUSIC_PATH, "Music/");
        }

        void initLogger()
        {

            FileTarget target = new FileTarget();
            target.AutoFlush = true;
            target.DeleteOldFileOnStartup = true;
            target.KeepFileOpen = true;
            target.CreateDirs = true;
            target.ConcurrentWrites = true; 
            target.FileAttributes = NLog.Internal.Win32.Win32FileAttributes.Normal;
            target.Header = "=================${newline}BEGIN BARRAGE LOG${newline}=================";
            target.Footer = "=================${newline} END BARRAGE LOG${newline}=================";
            target.LineEnding = FileTarget.LineEndingMode.CRLF;
            target.FileName = "Log/BarrageLog.txt";
            target.Layout = "${date:format=HH\\:mm\\:ss}\t${logger}\t\t${message}";
            target.BufferSize = 64 * 1024;
            target.Initialize();
            /*
            AsyncTargetWrapper wrapper = new AsyncTargetWrapper();
            wrapper.WrappedTarget = target;
            wrapper.QueueLimit = 500;
            wrapper.BatchSize = 250;
            wrapper.OverflowAction = AsyncTargetWrapperOverflowAction.Discard;
             */
            NLog.Config.SimpleConfigurator.ConfigureForTargetLogging(target, LogLevel.Debug);

        }


        void initMouse()
        {
            Mouse.control = this.mainForm;
            Mouse m = Mouse.Instance;
        }

        void initKeyboard()
        {
            SlimDX.DirectInput.DirectInput.Initialize();
            Keyboard.control = this.mainForm;
            Keyboard k = Keyboard.Instance;
        }

        void initD3D()
        {

            Cursor.Hide();
            Direct3D.Initialize();

            // Fill display modes
           
            DisplayModeCollection dmc = Direct3D.Adapters[0].GetDisplayModes(Format.X8R8G8B8);
            
            
            foreach (DisplayMode dm in dmc)
            {
                if (dm.RefreshRate != Direct3D.Adapters[0].CurrentDisplayMode.RefreshRate)
                    continue;
                if ((dm.Width == engineConfig.ResolutionWidth) && (dm.Height == engineConfig.ResolutionHeight))
                    currentDisplayMode = displayModes.Count;
                
                displayModes.Add(dm);
            }

            presentParameters = new PresentParameters();
            presentParameters.AutoDepthStencilFormat = Format.D24S8;
            presentParameters.BackBufferFormat = Format.X8R8G8B8;
            presentParameters.EnableAutoDepthStencil = true;
            presentParameters.BackBufferWidth = engineConfig.ResolutionWidth;
            presentParameters.BackBufferHeight = engineConfig.ResolutionHeight;
            mainForm.ClientSize = new System.Drawing.Size(engineConfig.ResolutionWidth, engineConfig.ResolutionHeight);
            presentParameters.DeviceWindowHandle = mainForm.Handle;
            presentParameters.Windowed = !engineConfig.FullScreen;
            if (engineConfig.FullScreen)
                presentParameters.FullScreenRefreshRateInHertz = 60;
            presentParameters.SwapEffect = SwapEffect.Discard;
            presentParameters.PresentationInterval = PresentInterval.Immediate;
            presentParameters.Multisample = MultisampleType.None;

            device = new Device(0, DeviceType.Hardware, mainForm.Handle, CreateFlags.NoWindowChanges | CreateFlags.PureDevice | CreateFlags.Multithreaded | CreateFlags.HardwareVertexProcessing, presentParameters);
            loadDeviceData();
            
            //Disable lost/reset exceptions
            //Configuration.AddResultWatch(ResultCode.DeviceLost, ResultWatchFlags.AlwaysIgnore);
            //Configuration.AddResultWatch(ResultCode.DeviceNotReset, ResultWatchFlags.AlwaysIgnore);


        }

        void loadDeviceData()
        {
            this.defaultRenderTargetDescription = device.GetRenderTarget(0).Description;
            this.defaultDepthBuffer = device.DepthStencilSurface;
            this.backBuffer = device.GetRenderTarget(0);
        }

        void unloadSurfaces()
        {
            backBuffer.Dispose();
            defaultDepthBuffer.Dispose();
        }

        /// <summary>
        /// Inits the game (not the engine) and starts the main loop
        /// </summary>
        /// <param name="game">Game that will be run</param>
        public void Run(Game game)
        {
            this.game = game;
            game.InitGame();
            mainForm.HandleDestroyed+= new EventHandler(Close);
            Application.Idle += new EventHandler(Loop);
            LogManager.GetLogger(ToString()).Info("Game starting");
            Application.Run(mainForm);
        }

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("User32.dll", CharSet = CharSet.Auto)]
        static extern bool PeekMessage(out Message msg, IntPtr hWnd, UInt32 msgFilterMin, UInt32 msgFilterMax, UInt32 flags);
        
        bool AppStillIdle
        {
            get
            {
                Message msg;
                return !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
            }
        }

        void Loop(object sender, EventArgs e)
        {
            while (AppStillIdle&&!Finished)
            {
                NLog.LogManager.GetLogger(ToString()).Info("Main loop begins");
                SingleLoop();
                NLog.LogManager.GetLogger(ToString()).Info("Main loop ends");
                if (Finished)
                    mainForm.Close();               
                Application.DoEvents();
            }
        }

        void Close(object sender, EventArgs e)
        {
            Close();
        }

        void Close()
        {
            NLog.LogManager.GetLogger(ToString()).Info("Closing Application");
            Dispose();
        }

        
        /// <summary>
        /// Clears resources and calls Dispose() on other engine sections. Also fires the OnDispose event
        /// </summary>
        public virtual void Dispose()
        {
            NLog.LogManager.GetLogger(ToString()).Debug("Disposing Engine");
            if (game != null)
                game.Dispose();
            game = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            OnDispose.Invoke();
            OnDispose = null;
            OnDisposeDefaultPool = null;
            OnRecreateDefaultPool = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            GC.WaitForPendingFinalizers();
            NLog.LogManager.GetLogger(ToString()).Debug("Disposing Device");
            defaultDepthBuffer.Dispose();
            defaultDepthBuffer = null;
            backBuffer.Dispose();
            backBuffer = null;
            device.Dispose();
            device = null;
            NLog.LogManager.GetLogger(ToString()).Debug("Everything disposed");
            NLog.LogManager.DisableLogging();
            GC.SuppressFinalize(this);
        }
        
        void SingleLoop()
        {
            if (!(mainForm.Focused)) return;
            Result currentStatus = device.TestCooperativeLevel();
            if (currentStatus == ResultCode.DeviceLost) 
                return;
            if (currentStatus == ResultCode.DeviceNotReset)
            {
                OnDisposeDefaultPool.Invoke();
                unloadSurfaces();
                device.Reset(presentParameters);
                loadDeviceData();
                OnRecreateDefaultPool.Invoke();
            }
            Keyboard.Instance.UpdateStatus();
            Mouse.Instance.UpdateStatus();
            Gamepad.UpdateStatus();
            Timer.DefaultTimer.Clock();
            NLog.LogManager.GetLogger(ToString()).Info("Time elapsed: " + Timer.DefaultTimer.LastInterval + "ms");
            game.MainLoop();
            game.UpdateScenes();
            RenderStats.ResetStats();
            foreach (RenderProcess rp in game.RenderProcesses)
            {
                rp.Render();
            }
            //DebugRenderProcess.Instance.Render();
            device.Present();
            if (game.Finished)
                finished = true;            
        }

        /// <summary>
        /// Destructor. Clears resources
        /// </summary>
        ~Engine()
        {
            Dispose();
        }
    }
}
